{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "parameters"
    ]
   },
   "outputs": [],
   "source": [
    "epochs = 10\n",
    "# このチュートリアルの目的はトレーニングそのものではな無いので、スピードアップのためにデータの一部しか使いません。\n",
    "# データセットをサイズを大きくした時にどうなるか興味のある方は以下の数字を大きくしてみてください。\n",
    "n_train_items = 640\n",
    "n_test_items = 640"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Part X - MNISTを使った暗号化環境下でのトレーニングと評価\n",
    "\n",
    "機械学習をベースにした何らかのサービス(MLaaS)を提供したいとします。そのためにはまずモデルのトレーニングを行う必要がありますが、そのためには外部のパートナー企業が保有するデータにアクセスする必要がある、なんて言う事はよくあります。一方で医療や金融の世界ではモデルもデータもとても重要です。モデルのパラメータはビジネスの根幹（知財）に関わりますし、ユーザーデータ（個人情報）の利用は厳しく規制されています。\n",
    "\n",
    "こういった状況下における潜在的な解決策の一つは、モデルもデータもどちらも暗号化し、暗号化されたままのデータでモデルのトレーニングを行うことです。この方法なら、モデルのトレーニングを行う企業はユーザーの個人情報を取得する必要はありませんし、サービスを利用する医療機関／金融機関も企業の知財であるモデル（のウェイト）を知る事はありません。\n",
    "\n",
    "暗号化されたデータを使ってのコンピューテーションを行うための手法はいくつか存在しますが、その中でも\"Secure Multi-Party Computation\" (SMPC)、\"Homomorphic Encryption\" (FHE/SHE) 、それに\"Functional Encryption\"(FE)は良く知られています。ここでは、チュートリアル5で紹介した\"Secure Multi-Party Computation\"を特に紹介します。Secure Multi-Party ComputationはSecureNNやSPDZといった暗号化プロトコルを使い、`shares`という概念によって暗号化を行います。`shares`秘密鍵のような数値を関係各社で分割して持ち回るような概念です。\n",
    "\n",
    "ところで、このチュートリアルの設定は次の通りです。あなたがサーバー（モデルのトレーニングを行う主体）で、N人のワーカーに分散しているデータを使ってモデルのトレーニングを行いたいとします。サーバーでは`shares`によってモデルを分割し、複数のワーカーに送信します。各ワーカーも自分たちのデータを`shares`によって分割し、ワーカー間で共有します。このケースではAliceとBobの2人です。`shares`を交換しあうことによって、各自は自身の`share`と他人の`share`を保持します。こうする事によって、サーバーは適切な暗号化プロトコルを使ってモデルのトーレニングを開始でき、トレーニングが終了後には、全ての`shares`をサーバーに戻すことによって、サーバーはモデルを複合化できます。次の画像はこの一連のプロセスを図示しています。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "![SMPC Illustration](https://github.com/OpenMined/PySyft/raw/11c85a121a1a136e354945686622ab3731246084/examples/tutorials/material/smpc_illustration.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "この設定の例として、AliceとBobがMNISTデータセットを分割して保持していると仮定します。では、モデルをトレーニングして、手書き文字の分類タスクをやってみましょう。\n",
    "\n",
    "Author:\n",
    "- Théo Ryffel - Twitter: [@theoryffel](https://twitter.com/theoryffel) · GitHub: [@LaRiffle](https://github.com/LaRiffle)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 1. MNISTを使った暗号化環境下でのモデル学習の例\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## PyTorch関連のライブラリのインポート"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "import torch.optim as optim\n",
    "from torchvision import datasets, transforms\n",
    "\n",
    "import time"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "トレーニングに関するハイパーパラメータを定義します。これらは全てパブリック変数です。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Arguments():\n",
    "    def __init__(self):\n",
    "        self.batch_size = 64\n",
    "        self.test_batch_size = 64\n",
    "        self.epochs = epochs\n",
    "        self.lr = 0.02\n",
    "        self.seed = 1\n",
    "        self.log_interval = 1 # 一つのバッチ毎にログを表示します\n",
    "        self.precision_fractional = 3\n",
    "\n",
    "args = Arguments()\n",
    "\n",
    "_ = torch.manual_seed(args.seed)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "PySyft関連のライブラリをインポートします。また、 `alice` と `bob`という名前でリモートワーカーを作成しておきましょう。更に暗号化プロトコルのプリミティブを提供するリモートワーカーを `crypto_provider`という名前で作成しておきます。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import syft as sy  # Pysyftライブラリをインポート\n",
    "hook = sy.TorchHook(torch)  # PyTorchをHookして、PyTorchを拡張します\n",
    "\n",
    "# simulation functions\n",
    "def connect_to_workers(n_workers):\n",
    "    return [\n",
    "        sy.VirtualWorker(hook, id=f\"worker{i+1}\")\n",
    "        for i in range(n_workers)\n",
    "    ]\n",
    "def connect_to_crypto_provider():\n",
    "    return sy.VirtualWorker(hook, id=\"crypto_provider\")\n",
    "\n",
    "workers = connect_to_workers(n_workers=2)\n",
    "crypto_provider = connect_to_crypto_provider()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## `shares`によって分割されたデータへのアクセス\n",
    "\n",
    "\n",
    "ではここで、ユーティリティ（便利な）関数を使って次の状況を実現しましょう。MNISTデータセットはバラバラに複数のリモートワーカーによって保持されていると仮定していた事を思い出してください。ワーカーは自分のデータをバッチサイズに分割し、`shares`によって暗号化の上、`shares`を互いに持ち合います。最終的に返されるオブジェクトは`shares`によって暗号化されたデータセットの`iterable`です。私たちはこれを**private data loader**と呼びます。\n",
    "\n",
    "注記: このプロセス全体を通してローカルワーカー（サーバー、ここでは私たち）は決してデータを見ることはありません。\n",
    "\n",
    "次に暗号化されたデータセットを使って今までと同様の手順でトレーニングを実行します。入力データも正解ラベルも`shares`によって暗号化されています。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_private_data_loaders(precision_fractional, workers, crypto_provider):\n",
    "    \n",
    "    def one_hot_of(index_tensor):\n",
    "        \"\"\"\n",
    "        Transform to one hot tensor\n",
    "        \n",
    "        Example:\n",
    "            [0, 3, 9]\n",
    "            =>\n",
    "            [[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n",
    "             [0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],\n",
    "             [0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]]\n",
    "            \n",
    "        \"\"\"\n",
    "        onehot_tensor = torch.zeros(*index_tensor.shape, 10) # 10 classes for MNIST\n",
    "        onehot_tensor = onehot_tensor.scatter(1, index_tensor.view(-1, 1), 1)\n",
    "        return onehot_tensor\n",
    "        \n",
    "    def secret_share(tensor):\n",
    "        \"\"\"\n",
    "        Transform to fixed precision and secret share a tensor\n",
    "        \"\"\"\n",
    "        return (\n",
    "            tensor\n",
    "            .fix_precision(precision_fractional=precision_fractional)\n",
    "            .share(*workers, crypto_provider=crypto_provider, requires_grad=True)\n",
    "        )\n",
    "    \n",
    "    transformation = transforms.Compose([\n",
    "        transforms.ToTensor(),\n",
    "        transforms.Normalize((0.1307,), (0.3081,))\n",
    "    ])\n",
    "    \n",
    "    train_loader = torch.utils.data.DataLoader(\n",
    "        datasets.MNIST('../data', train=True, download=True, transform=transformation),\n",
    "        batch_size=args.batch_size\n",
    "    )\n",
    "    \n",
    "    private_train_loader = [\n",
    "        (secret_share(data), secret_share(one_hot_of(target))) for i, (data, target) in enumerate(train_loader) if i < n_train_items / args.batch_size\n",
    "    ]\n",
    "    \n",
    "    test_loader = torch.utils.data.DataLoader(\n",
    "        datasets.MNIST('../data', train=False, download=True, transform=transformation),\n",
    "        batch_size=args.test_batch_size\n",
    "    )\n",
    "    \n",
    "    private_test_loader = [\n",
    "        (secret_share(data), secret_share(target.float()))\n",
    "        for i, (data, target) in enumerate(test_loader)\n",
    "        if i < n_test_items / args.test_batch_size\n",
    "    ]\n",
    "    \n",
    "    return private_train_loader, private_test_loader\n",
    "    \n",
    "    \n",
    "private_train_loader, private_test_loader = get_private_data_loaders(\n",
    "    precision_fractional=args.precision_fractional,\n",
    "    workers=workers,\n",
    "    crypto_provider=crypto_provider\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## モデル定義\n",
    "\n",
    "これは今回使うモデルです。シンプルなモデルですが、まずまず良い感じに動作する事が知られています。\n",
    "詳細は[it has proved to perform reasonably well on MNIST](https://towardsdatascience.com/handwritten-digit-mnist-pytorch-977b5338e627)を参照してください。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Net(nn.Module):\n",
    "    def __init__(self):\n",
    "        super(Net, self).__init__()\n",
    "        self.fc1 = nn.Linear(28 * 28, 128)\n",
    "        self.fc2 = nn.Linear(128, 64)\n",
    "        self.fc3 = nn.Linear(64, 10)\n",
    "\n",
    "    def forward(self, x):\n",
    "        x = x.view(-1, 28 * 28)\n",
    "        x = F.relu(self.fc1(x))\n",
    "        x = F.relu(self.fc2(x))\n",
    "        x = self.fc3(x)\n",
    "        return x"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## トレーニング関数とテスト関数\n",
    "\n",
    "トレーニングはほとんどいつもと同じです。異なる点は、negative log-likelihood (`F.nll_loss` in PyTorch) をロス関数として使えない事です。negative log-likelihoodは複雑過ぎてSMPC環境下では再現が難しいため、代わりにもっと簡単なMean Square Errorを使います。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def train(args, model, private_train_loader, optimizer, epoch):\n",
    "    model.train()\n",
    "    for batch_idx, (data, target) in enumerate(private_train_loader): # <-- プライバシーに配慮したデータローダーです\n",
    "        start_time = time.time()\n",
    "        \n",
    "        optimizer.zero_grad()\n",
    "        \n",
    "        output = model(data)\n",
    "        \n",
    "        # loss = F.nll_loss(output, target)  <-- SMPC環境下では再現が難しいため、変更します\n",
    "        batch_size = output.shape[0]\n",
    "        loss = ((output - target)**2).sum().refresh()/batch_size\n",
    "        \n",
    "        loss.backward()\n",
    "        \n",
    "        optimizer.step()\n",
    "\n",
    "        if batch_idx % args.log_interval == 0:\n",
    "            loss = loss.get().float_precision()\n",
    "            print('Train Epoch: {} [{}/{} ({:.0f}%)]\\tLoss: {:.6f}\\tTime: {:.3f}s'.format(\n",
    "                epoch, batch_idx * args.batch_size, len(private_train_loader) * args.batch_size,\n",
    "                100. * batch_idx / len(private_train_loader), loss.item(), time.time() - start_time))\n",
    "            "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "テスト関数に変更はありません"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def test(args, model, private_test_loader):\n",
    "    model.eval()\n",
    "    test_loss = 0\n",
    "    correct = 0\n",
    "    with torch.no_grad():\n",
    "        for data, target in private_test_loader:\n",
    "            start_time = time.time()\n",
    "            \n",
    "            output = model(data)\n",
    "            pred = output.argmax(dim=1)\n",
    "            correct += pred.eq(target.view_as(pred)).sum()\n",
    "\n",
    "    correct = correct.get().float_precision()\n",
    "    print('\\nTest set: Accuracy: {}/{} ({:.0f}%)\\n'.format(\n",
    "        correct.item(), len(private_test_loader)* args.test_batch_size,\n",
    "        100. * correct.item() / (len(private_test_loader) * args.test_batch_size)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### トレーニングの実行\n",
    "\n",
    "何点か補足すべき点があります。まず、私たちはモデルのパラメータ（ウェイト）を`shares`でにって暗号化し、リモートワーカーと共有しています。次にオプティマイザで使用するハイパーパラメータを整数にしています（fixed precision）。ハイパーパラメータ自体はパブリック変数なので暗号化をかける必要はありませんが、他の変数が整数になっているので、そこに揃えて整数化する必要があります。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = Net()\n",
    "model = model.fix_precision().share(*workers, crypto_provider=crypto_provider, requires_grad=True)\n",
    "\n",
    "optimizer = optim.SGD(model.parameters(), lr=args.lr)\n",
    "optimizer = optimizer.fix_precision() \n",
    "\n",
    "for epoch in range(1, args.epochs + 1):\n",
    "    train(args, model, private_train_loader, optimizer, epoch)\n",
    "    test(args, model, private_test_loader)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "ジャジャーン！全てが暗号化された環境下で、MNISTのトレーニングをする事ができました。データは全体のごく一部しか使っていませんが、75％の精度を達成する事ができています。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 2. 考察\n",
    "\n",
    "たった今行ったトレーニングを細かく分析してみましょう。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2.1 コンピューテーションに掛かる時刻\n",
    "\n",
    "第1はコンピューテーションに掛かる時間です。もう既に気がついていることと思いますが、プレーンのトレーニングと比べて随分と遅いです。バッチサイズ、64、でバッチあたりの時間が、ピュアなPyTorchなら13ミリ秒しか掛からないところを、3.2秒も掛かっています。もちろんこれは大きな問題でです。ですが、思い出してください。私たちは全てをリモート、かつ暗号化された環境下で行いました。ただの一つのデータさせ流出はさせていません。1つのデータ処理に掛かる時間は50ミリ秒ほどです。それほど悪くないと思いませんか？ここで問うべき問題は、暗号化環境下でのトレーニングしか選択肢がない時に、使えるか使えないかという事です。例えば実運用環境で、推論に50ミリ秒なら全然問題ありませんよね。\n",
    "\n",
    "ところで、一番のボトルネックはアクティベーション関数です。例えばディープラーニングでよく使われるreluですが、暗号化環境下での\"比較\"とSecureNNプロトコルを使用するので、時間が掛かります。CryptoNetsなどの暗号化環境下でのコンピューテーション関連の論文に見られるようにreluをquadraticアクティベーションで置き換えると3.2秒掛かっていたバッチ処理が1.2秒になったという報告もあります。\n",
    "\n",
    "いずれにせよ、現時点でのベストプラクティスとしては、必要な部分だけを暗号化するということです。次のチュートリアルではその方法を紹介します。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2.2 SMPCでのバックプロパゲーション\n",
    "\n",
    "ところで、データが暗号化された環境では整数で計算を行うのにどうやって勾配ベクトルを計算しているんだって疑問に思うかもしれません。暗号化環境下での勾配ベクトルのアップデートを実現するため、私たちはAutogradTensorと呼ばれる新しいオブジェクトを開発しました。皆さんは裏で何が起きているのか理解できないかもしれませんが、次のチュートリアルではこれを随所に使います。まずはprint関数でモデルのウェイトを表示してみましょう。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model.fc3.bias"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "データも覗いてみましょう"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "first_batch, input_data = 0, 0\n",
    "private_train_loader[first_batch][input_data]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "ご覧の通り、AutogradTensorが登場しています。FixedPrecisionTensorとPyTorchのTensorのラッパーの間に存在しています。これはデータが整数として存在していることを示唆しています。AutogradTensorの目的は、暗号化されたデータでの演算が行われる際に、コンピューテーショングラフを保持することにあります。AutogradTensorは、そのままでは整数では動かない全ての関連関数を上書きます。\n",
    "\n",
    "例としては、Beaver triples trickを使って実装されている乗算などがあります。乗算の微分以上の複雑な微分を自分でやるは避けたいですよね。$\\partial_b (a \\cdot b) = a \\cdot \\partial b$\n",
    "\n",
    "ここに微分計算の実装の例を挙げておきます。\n",
    "\n",
    "```python\n",
    "class MulBackward(GradFunc):\n",
    "    def __init__(self, self_, other):\n",
    "        super().__init__(self, self_, other)\n",
    "        self.self_ = self_\n",
    "        self.other = other\n",
    "\n",
    "    def gradient(self, grad):\n",
    "        grad_self_ = grad * self.other\n",
    "        grad_other = grad * self.self_ if type(self.self_) == type(self.other) else None\n",
    "        return (grad_self_, grad_other)\n",
    "```\n",
    "\n",
    "勾配ベクトル関連の実装に興味がある方は`tensors/interpreters/gradients.py`を参照してください。\n",
    "\n",
    "ところで、コンピューテーショングラフに関してですが、コピーはローカルに存在し、フォーワードとバックワードの指示はサーバーから受けます。これは想定通りですね。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2.3 機密保持の保証に関して\n",
    "\n",
    "\n",
    "ここで、セキュリティについて一つ言及しておきましょう。私たちが想定しているのは、**悪意は無い**相手です。**悪意が無い**の意味は、データやモデルが暗号化されずに送られてきてしまったら興味本位で見てしまう可能性はあるけど、意図的に全てを台無しにしようと思っているわけではな無い、という事です。というのも、PySyftやSMPCを使えば、相手はデータの中身を見ることは出来ないけれど、悪意があれば、`shares`を改編するなどしてトレーニングをストップさせることは可能です。SMPCでは悪意を持った相手に対する対応策は現在ありません。\n",
    "\n",
    "これに加えて、Secure Multi-Party Computationを使うことによってデータそのものへのアクセスは出来なくなっていたとしても、平文で入力データをサーバーに送れば入力データや推論結果が盗み見られてしまう危険はあります。そして、その入力データによっては何がトレーニングデータとして使われたのか明らかになってしまう場合もあります。得に、具体的な学習データを復元させようとするmembership attacksと呼ばれる攻撃手法に対しては（特別な）防御メカニズムを持っていません。また、モデルが予期せぬ、本来覚えるべきでないことを学んでしまうケースや、model inversion attackに対する脆弱性は一般のディープラーニングと同じです。\n",
    "\n",
    "これらの脅威に対する一般的な対応策は、Differential Privacyを使うことです。Secure Multi-Party Computationとの相性もよく、とても興味深いセキュリティを提供します。私たちは現在Differential Privacyを使った実装に取り組んでいる最中です。準備が出来たら応用例を紹介します。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 結論\n",
    "\n",
    "SMPCを使ったモデルのトレーニングは、内部的には複雑なことをやっているのですが、コードという点では難しいものではありません。\n",
    "これを踏まえて、どんな時に暗号化されたデータでのディープラーニングが必要なのか、是非考えてみてください。暗号化されたデータでのディープラーニングは通常のそれより遅いですから、どんな時にコンピューテーションのコストが正当化できるを考えるのはとても重要なことです。\n",
    "\n",
    "もし、このチュートリアルを気に入って、プライバシーに配慮した非中央集権的なAI技術や付随する（データやモデルの）サプライチェーンにご興味があって、プロジェクトに参加したいと思われるなら、以下の方法で可能です。\n",
    "\n",
    "### PySyftのGitHubレポジトリにスターをつける\n",
    "\n",
    "一番簡単に貢献できる方法はこのGitHubのレポジトリにスターを付けていただくことです。スターが増えると露出が増え、より多くのデベロッパーにこのクールな技術の事を知って貰えます。\n",
    "\n",
    "- [Star PySyft](https://github.com/OpenMined/PySyft)\n",
    "\n",
    "### Slackに入る\n",
    "\n",
    "最新の開発状況のトラッキングする一番良い方法はSlackに入ることです。\n",
    "下記フォームから入る事ができます。\n",
    "[http://slack.openmined.org](http://slack.openmined.org)\n",
    "\n",
    "### コードプロジェクトに参加する\n",
    "\n",
    "コミュニティに貢献する一番良い方法はソースコードのコントリビューターになることです。PySyftのGitHubへアクセスしてIssueのページを開き、\"Projects\"で検索してみてください。参加し得るプロジェクトの状況を把握することができます。また、\"good first issue\"とマークされているIssueを探す事でミニプロジェクトを探すこともできます。\n",
    "\n",
    "- [PySyft Projects](https://github.com/OpenMined/PySyft/issues?q=is%3Aopen+is%3Aissue+label%3AProject)\n",
    "- [Good First Issue Tickets](https://github.com/OpenMined/PySyft/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)\n",
    "\n",
    "### 寄付\n",
    "\n",
    "もし、ソースコードで貢献できるほどの時間は取れないけど、是非何かサポートしたいという場合は、寄付をしていただくことも可能です。寄附金の全ては、ハッカソンやミートアップの開催といった、コミュニティ運営経費として利用されます。\n",
    "\n",
    "[OpenMined's Open Collective Page](https://opencollective.com/openmined)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
